// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:compiler/src/commandline_options.dart';
import 'package:compiler/src/common/elements.dart';
import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/elements/names.dart';
import 'package:compiler/src/js_backend/runtime_types_resolution.dart';
import 'package:compiler/src/js_emitter/model.dart';
import 'package:compiler/src/js_model/js_strategy.dart';
import 'package:compiler/src/js/js.dart' as js;
import 'package:compiler/src/js_model/js_world.dart' show JClosedWorld;
import 'package:compiler/src/universe/call_structure.dart';
import 'package:compiler/src/universe/selector.dart';
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import '../helpers/program_lookup.dart';
import 'package:compiler/src/util/memory_compiler.dart';

const String code = '''
class A {
  // Both method1 implementations need type arguments.
  @pragma('dart2js:noInline')
  method1<T>(T t) => t is T;

  // One of the method2 implementations need type arguments.
  @pragma('dart2js:noInline')
  method2<T>(T t) => t is T;

  // None of the method3 implementations need type arguments.
  @pragma('dart2js:noInline')
  method3<T>(T t) => false;
}

class B {
  @pragma('dart2js:noInline')
  method1<T>(T t) => t is T;
  @pragma('dart2js:noInline')
  method2<T>(T t) => true;
  @pragma('dart2js:noInline')
  method3<T>(T t) => true;
}

// A call to either A.method1 or B.method1.
@pragma('dart2js:noInline')
call1(c) => c.method1<int>(0);

// A call to A.method1.
@pragma('dart2js:noInline')
call1a() => A().method1<int>(0);

// A call to B.method1.
@pragma('dart2js:noInline')
call1b() => B().method1<int>(0);

// A call to either A.method2 or B.method2.
@pragma('dart2js:noInline')
call2(c) => c.method2<int>(0);

// A call to A.method2.
@pragma('dart2js:noInline')
call2a() => A().method2<int>(0);

// A call to B.method2.
@pragma('dart2js:noInline')
call2b() => B().method2<int>(0);

// A call to either A.method3 or B.method3.
@pragma('dart2js:noInline')
call3(c) => c.method3<int>(0);

// A call to A.method3.
@pragma('dart2js:noInline')
call3a() => A().method3<int>(0);

// A call to B.method3.
@pragma('dart2js:noInline')
call3b() => B().method3<int>(0);

main() {
  call1(new A());
  call1(new B());
  call1a();
  call1b();
  call2(new A());
  call2(new B());
  call2a();
  call2b();
  call3(new A());
  call3(new B());
  call3a();
  call3b();
}
''';

main() {
  asyncTest(() async {
    CompilationResult result = await runCompiler(
      memorySourceFiles: {'main.dart': code},
      options: [Flags.omitImplicitChecks],
    );
    Expect.isTrue(result.isSuccess);
    Compiler compiler = result.compiler!;
    JsBackendStrategy backendStrategy = compiler.backendStrategy;
    JClosedWorld closedWorld = compiler.backendClosedWorldForTesting!;
    RuntimeTypesNeed rtiNeed = closedWorld.rtiNeed;
    ElementEnvironment elementEnvironment = closedWorld.elementEnvironment;
    ProgramLookup programLookup = ProgramLookup(backendStrategy);

    js.Name getName(String name, int typeArguments) {
      return backendStrategy.namerForTesting.invocationName(
        new Selector.call(
          PublicName(name),
          CallStructure(1, const <String>[], typeArguments),
        ),
      );
    }

    void checkParameters(
      String name, {
      required int expectedParameterCount,
      required bool needsTypeArguments,
    }) {
      final function = lookupMember(elementEnvironment, name) as FunctionEntity;

      Expect.equals(
        needsTypeArguments,
        rtiNeed.methodNeedsTypeArguments(function),
        "Unexpected type argument need for $function.",
      );
      Method method = programLookup.getMethod(function)!;

      final fun = method.code as js.Fun;
      Expect.equals(
        expectedParameterCount,
        fun.params.length,
        "Unexpected parameter count on $function: ${js.nodeToString(fun)}",
      );
    }

    // The declarations should have type parameters only when needed.
    checkParameters(
      'A.method1',
      expectedParameterCount: 2,
      needsTypeArguments: true,
    );
    checkParameters(
      'B.method1',
      expectedParameterCount: 2,
      needsTypeArguments: true,
    );
    checkParameters(
      'A.method2',
      expectedParameterCount: 2,
      needsTypeArguments: true,
    );
    checkParameters(
      'B.method2',
      expectedParameterCount: 1,
      needsTypeArguments: false,
    );
    checkParameters(
      'A.method3',
      expectedParameterCount: 1,
      needsTypeArguments: false,
    );
    checkParameters(
      'B.method3',
      expectedParameterCount: 1,
      needsTypeArguments: false,
    );

    checkArguments(
      String name,
      String targetName, {
      required int expectedTypeArguments,
    }) {
      final function = lookupMember(elementEnvironment, name) as FunctionEntity;
      Method method = programLookup.getMethod(function)!;

      final fun = method.code as js.Fun;

      js.Name selector = getName(targetName, expectedTypeArguments);
      bool callFound = false;
      forEachNode(
        fun,
        onCall: (js.Call node) {
          js.Node target = js.undefer(node.target);
          if (target is js.PropertyAccess) {
            js.Node targetSelector = js.undefer(target.selector);
            if (targetSelector is js.Name &&
                targetSelector.key == selector.key) {
              callFound = true;
              Expect.equals(
                1 + expectedTypeArguments,
                node.arguments.length,
                "Unexpected argument count in $function call to $targetName: "
                "${js.nodeToString(fun)}",
              );
            }
          }
        },
      );
      Expect.isTrue(
        callFound,
        "No call to $targetName as '${selector.key}' in $function found.",
      );
    }

    // The declarations should have type parameters only when needed by the
    // selector.
    checkArguments('call1', 'method1', expectedTypeArguments: 1);
    checkArguments('call1a', 'method1', expectedTypeArguments: 1);
    checkArguments('call1b', 'method1', expectedTypeArguments: 1);
    checkArguments('call2', 'method2', expectedTypeArguments: 1);
    checkArguments('call2a', 'method2', expectedTypeArguments: 1);
    checkArguments('call2b', 'method2', expectedTypeArguments: 1);
    checkArguments('call3', 'method3', expectedTypeArguments: 0);
    checkArguments('call3a', 'method3', expectedTypeArguments: 0);
    checkArguments('call3b', 'method3', expectedTypeArguments: 0);
  });
}
